/* 
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Managers;
using DCSharp.Backend.Objects;

using File = System.IO.File;
using Directory = System.IO.Directory;

namespace DCSharp.Backend.Protocols.Nmdc
{
	public class UserProtocol : Protocol, IUserProtocol
	{
		[Flags]
		public enum Features
		{
			MiniSlots = 0x01,
			BZList = 0x02,
			XmlBZList = 0x04,
			ADCGet = 0x08,
			GetZBlock = 0x10,
			GetTestZBlock = 0x20,
			ZLIG = 0x40,
			TTHL = 0x80,
			TTHF = 0x100,
			CDM = 0x200
		}

		protected enum States
		{
			// Handshake
			Nick,
			Lock,
			Direction,
			Key,

			// Upload
			Get,
			Send,

			// Download
			FileLength
		}

		protected enum Errors
		{
			FileNotAvailable,
			InvalidCommand,
			InvalidKey,
			NoSlots
		}

		private UserConnection connection;
		private IUserProtocolHelper helper;

		private Features features;
		private Incoming incoming;

		private TransferDirection direction;
		private Request request;

		public UserProtocol(UserConnection connection, IUserProtocolHelper helper)
		{
			if (connection == null)
			{
				throw new ArgumentNullException("connection");
			}
			if (helper == null)
			{
				throw new ArgumentNullException("helper");
			}
			this.connection = connection;
			this.helper = helper;

			incoming = new Incoming(this);

			// Handshake
			commands.Add("$MyNick", incoming.MyNick);
			commands.Add("$Lock", incoming.Lock);
			commands.Add("$Supports", incoming.Supports);
			commands.Add("$Direction", incoming.Direction);
			commands.Add("$Key", incoming.Key);

			// Upload
			commands.Add("$Get", incoming.Get);
			commands.Add("$Send", incoming.Send);
			commands.Add("$GetListLen", incoming.GetListLen);
			commands.Add("$ADCGET", incoming.AdcGet);

			// Download
			commands.Add("$FileLength", incoming.FileLength);
			commands.Add("$MaxedOut", incoming.MaxedOut);
			commands.Add("$ADCSND", incoming.AdcSnd);

			// Other
			commands.Add("$Error", incoming.Error);
		}

		#region Classes

		protected class Incoming
		{
			private UserProtocol protocol;
			public States state;

			public string keyStr;
			private int directionNumber;

			private Request request;
			private Stream response;

			public Incoming(UserProtocol protocol)
			{
				this.protocol = protocol;

				state = States.Nick;
				directionNumber = new Random().Next(32767);
			}

			#region Methods

			#region Handshake

			public void MyNick(string nick)
			{
				CheckState(States.Nick);

				nick = UnescapeNick(nick);

				HubConnection hub = protocol.Connection.Hub;
				User user = null;

				// Get the hub and user
				if (hub != null)
				{
					Identity identity = protocol.GetIdentity(hub, nick);
					user = identity != null ? identity.User : null;
				}
				else
				{
					protocol.Helper.IdentifyUser(nick, null, null, out user,
						out hub);
				}

				if (user == null || hub == null)
				{
					Debug.WriteLine("Unknown user " + nick);
					protocol.Connection.Disconnect();
					return;
				}

				// Update the connection
				protocol.Connection.RemoteUser = user;

				protocol.Connection.Hub = hub;
				protocol.Connection.LocalIdentity = hub.LocalIdentity;
				protocol.Connection.Encoding = hub.Encoding;

				// Direction
				protocol.direction = protocol.Helper.GetTransferDirection(user);

				// Done
				if (protocol.Connection.Incoming)
				{
					protocol.MyNick();
					protocol.Lock();
				}
				state = States.Lock;
			}

			public void Lock(string argument)
			{
				CheckState(States.Lock);

				protocol.Supports();
				protocol.Direction(directionNumber);
				Protocol.SendKey(protocol.Connection, CalculateKey(argument, protocol.Encoding));

				state = States.Direction;
			}

			public void Supports(string argument)
			{
				foreach(string feature in argument.Split(' '))
				{
					if (feature.Length > 0)
					{
						try
						{
							Features feat = (Features)Enum.Parse(typeof(Features),
								feature);

							protocol.features |= feat;
						}
						catch(ArgumentException)
						{
							Debug.WriteLine(feature + " needs to be added to features.");
						}
					}
				}
			}

			public void Direction(string argument)
			{
				CheckState(States.Direction);

				string[] args = argument.Split(' ');
				if (args[0].Equals("Upload") &&
					protocol.direction == TransferDirection.Up)
				{
					Debug.WriteLine("No one wants to download");
					protocol.Connection.Disconnect();
				}
				else if (args[0].Equals("Download") &&
					protocol.direction == TransferDirection.Down)
				{
					// The one with the highest number gets to download
					int number = int.Parse(args[1]);
					if (number > directionNumber)
					{
						protocol.direction = TransferDirection.Up;
					}
					else if (number == directionNumber)
					{
						Debug.WriteLine("$Direction collision");
						protocol.Connection.Disconnect();
					}
				}
				state = States.Key;
			}

			public void Key(string argument)
			{
				CheckState(States.Key);

				// Get the local key in the connection encoding.
				// TODO: Compare with the original incoming byte array instead?
				byte[] bytes = new byte[keyStr.Length];
				for (int i = 0; i < keyStr.Length; i++)
				{
					bytes[i] = Convert.ToByte(keyStr[i]);
				}
				string key = protocol.Encoding.GetString(bytes);

				if (argument != key)
				{
					protocol.Error(Errors.InvalidKey);
					protocol.Connection.Disconnect();
					return;
				}

				// Handshake done!
				state = protocol.direction == TransferDirection.Down ?
					state = States.FileLength : state = States.Get;

				protocol.Connection.Direction = protocol.direction;
				protocol.Connection.SetStateConnected();
			}

			#endregion

			#region Upload

			public void Get(string argument)
			{
				CheckState(States.Get);

				string[] strings = argument.Split('$');

				long start;
				if (strings.Length != 2 ||
					!long.TryParse(strings[1].Trim(), out start) || start < 1)
				{
					protocol.Error(Errors.InvalidCommand);
					return;
				}

				string path = Protocol.FixSeparators(strings[0].Trim());
				start = start - 1;
				try
				{
					request = CreateRequest(path, null, start, -1);
					if (!GetResponse(request, out response))
					{
						protocol.Error(Errors.FileNotAvailable);
						return;
					}
					protocol.Send("$FileLength {0}|", response.Length);
					state = States.Send;
				}
				catch (FileNotFoundException)
				{
					protocol.Error(Errors.FileNotAvailable);
				}
				catch (NoSlotsException)
				{
					protocol.MaxedOut();
				}
			}

			public void Send(string argument)
			{
				CheckState(States.Send);

				long count = response.Length - response.Position;
				protocol.Connection.Upload(request, response, count);

				request = null;
				response = null;

				state = States.Get;
			}

			// $GetListLen returns the length of the DcLst style file list
			public void GetListLen(string argument)
			{
				CheckState(States.Get);

				// 42 is the response used by DC++
				protocol.Send("$ListLen 42|");
			}

			public void AdcGet(string argument)
			{
				CheckState(States.Get);
				EnsureUTF8(ref argument);

				// Split on spaces not escaped with a backslash.
				string[] parameters = Regex.Split(argument, @"(?<!\\) ");

				long start;
				long count;
				if (parameters.Length < 4 ||
					!long.TryParse(parameters[2], out start) ||
					!long.TryParse(parameters[3], out count))
				{
					protocol.Error(Errors.InvalidCommand);
					return;
				}

				string type = parameters[0];
				string path = parameters[1];
				bool compress = parameters.Length >= 5 && parameters[4] == "ZL1";

				// Create the request
				Request request;
				if (type == "file" && start >= 0 && (count > 0 || count == -1))
				{
					string p = null;
					string tth = path.StartsWith("TTH/") ? path.Substring(4) : null;

					if (tth == null)
					{
						p = path.StartsWith("/") ? path.Substring(1) : path;
						p = Protocol.FixSeparators(UserProtocol.UnescapeAdcParameter(p));
					}
					request = CreateRequest(p, tth, start, count);
				}
				else if (type == "tthl" && path.StartsWith("TTH/") &&
					start == 0 && count == -1)
				{
					request = new HashTreeRequest(path.Substring(4));
				}
				else
				{
					protocol.Error(Errors.InvalidCommand);
					return;
				}

				// Upload
				try
				{
					Stream response;
					if (!GetResponse(request, out response))
					{
						protocol.Error(Errors.FileNotAvailable);
						return;
					}
					if (count == -1)
					{
						count = response.Length - response.Position;
					}

					protocol.Send(Encoding.UTF8, "$ADCSND {0} {1} {2} {3}{4}|",
						type, path, start, count, compress ? " ZL1" : null);

					protocol.Connection.Upload(request, response, count, compress);
				}
				catch (FileNotFoundException)
				{
					protocol.Error(Errors.FileNotAvailable);
				}
				catch (NoSlotsException)
				{
					protocol.MaxedOut();
				}
			}

			#endregion

			#region Download

			public void FileLength(string argument)
			{
				CheckState(States.FileLength);

				FileRequest fileRequest = protocol.request as FileRequest;

				long length;
				if (!long.TryParse(argument, out length) || length <= 0 ||
					(fileRequest != null && fileRequest.Start + fileRequest.Count < length))
				{
					protocol.Connection.DownloadError(TransferError.Unknown,
						"Invalid response.");
					return;
				}
				if (fileRequest != null)
				{
					length -= fileRequest.Start;
				}

				protocol.Send("$Send|");
				protocol.Connection.BeginReceive(length);
			}

			public void MaxedOut(string argument)
			{
				CheckState(States.FileLength);

				protocol.Connection.DownloadError(TransferError.NoSlots,
					"No slots available");
			}

			public void AdcSnd(string argument)
			{
				CheckState(States.FileLength);
				EnsureUTF8(ref argument);

				// Split on spaces not escaped with a backslash.
				string[] args = Regex.Split(argument, @"(?<!\\) ");

				long start;
				long count;
				if (args.Length < 4 ||
					!long.TryParse(args[2], out start) ||
					!long.TryParse(args[3], out count) ||
					!IsValidResponse(protocol.request, start, count))
				{
					protocol.Connection.DownloadError(TransferError.Unknown,
						"Invalid response.");
					return;
				}
				protocol.Connection.BeginReceive(count);
			}

			private static bool IsValidResponse(Request request, long start,
				long count)
			{
				FileRequest fileRequest = request as FileRequest;
				if (fileRequest != null)
				{
					return start == fileRequest.Start &&
						count == fileRequest.Count;
				}
				else
				{
					return start == 0 && count >= 1;
				}
			}

			#endregion

			#region Other

			public void Error(string argument)
			{
				if (state == States.FileLength)
				{
					string arg = argument.ToLower();
					if (arg.IndexOf(" no more exists") >= 0)
					{
						protocol.Connection.DownloadError(TransferError.NoSlots, argument);
					}
					else if (arg == "file not available")
					{
						protocol.Connection.DownloadError(TransferError.NotAvailable, argument);
					}
					else
					{
						protocol.Connection.DownloadError(TransferError.Unknown, argument);
					}
				}
			}

			#endregion

			protected void CheckState(States assumedState)
			{
				if (assumedState != state)
				{
					throw new InvalidOperationException();
				}
			}

			protected void EnsureUTF8(ref string argument)
			{
				if (protocol.Encoding.CodePage != 65001)
				{
					byte[] data = protocol.Encoding.GetBytes(argument);
					argument = System.Text.Encoding.UTF8.GetString(data);
				}
			}

			protected static Request CreateRequest(string path, string tth,
				long start, long count)
			{
				if (path == "files.xml.bz2")
				{
					return new ListRequest(true);
				}
				else if (path == "files.xml")
				{
					return new ListRequest(false);
				}
				return new FileRequest(path, tth, start,
					count > 0 ? (long?)count : (long?)null);
			}

			protected bool GetResponse(Request request, out Stream response)
			{
				if (request != null)
				{
					UploadRequestEventArgs args = new UploadRequestEventArgs(request);

					protocol.Connection.EmitUploadRequest(args);
					response = args.Response;

					return response != null;
				}
				throw new FileNotFoundException();
			}
		}

		#endregion

		#region Properties

		public UserConnection Connection
		{
			get { return connection; }
			set { connection = value; }
		}

		public IUserProtocolHelper Helper
		{
			get { return helper; }
		}

		#endregion

		public override void Initialize()
		{
			base.Initialize();

			if (!connection.Incoming)
			{
				MyNick();
				Lock();
			}
		}

		protected override void Invoke(string command, string argument)
		{
			try
			{
				base.Invoke(command, argument);
			}
			catch (Exception e)
			{
				Debug.WriteLine(e);
				Error(Errors.InvalidCommand);

				// TODO: Hold the previous download
				if (direction == TransferDirection.Down)
				{
					incoming.state = States.FileLength;
				}
				else
				{
					incoming.state = States.Get;
				}
			}
		}

		public bool Supports(RequestType type)
		{
			switch (type)
			{
				case RequestType.File:
					// return Supports(Features.ADCGet & Features.TTHF)
					return true;

				case RequestType.List:
					return Supports(Features.XmlBZList);

				case RequestType.HashTree:
					return Supports(Features.ADCGet & Features.TTHL);
			}
			return false;
		}

		protected bool Supports(Features feature)
		{
			return features == (features | feature);
		}

		#region Handshake

		protected void MyNick()
		{
			Send("$MyNick {0}|", EscapeNick(connection.LocalIdentity.Nick));
		}

		protected void Lock()
		{
			string lockStr = "EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.674ABCABC";
			incoming.keyStr = CalculateKey(lockStr, Encoding);

			Send("$Lock {0}|", lockStr);
		}

		protected void Supports()
		{
			// If we send a lock string starting with EXTENDEDPROTOCOL we have
			// to support at least XmlBZList and UGetBlock
			string[] features = new string[] {"MiniSlots", "XmlBZList",
				"ADCGet", "TTHL", "TTHF"}; //, "GetZBlock", "ZLIG"};

			string supports = String.Empty;
			foreach(string feature in features)
			{
				supports += feature + " ";
			}
			Send("$Supports {0}|", supports);
		}

		protected void Direction(int number)
		{
			Send("$Direction {0} {1}|",
				direction == TransferDirection.Down ? "Download" : "Upload",
				number);
		}

		#endregion

		#region Download

		public void Download(Request request)
		{
			if (request == null)
			{
				throw new ArgumentNullException("request");
			}
			if (!Supports(request.Type))
			{
				throw new ArgumentException("Request not supported");
			}
			this.request = request;

			// Send the command to get the file
			FileRequest fileRequest = request as FileRequest;
			if (fileRequest != null)
			{
				if (fileRequest.TTH != null &&
					Supports(Features.ADCGet & Features.TTHF))
				{
					ADCGET("file", "TTH/" + fileRequest.TTH, fileRequest.Start,
						fileRequest.Count ?? -1);
				}
				else if (fileRequest.Path != null)
				{
					if (Supports(Features.ADCGet))
					{
						ADCGET("file", fileRequest.Path, fileRequest.Start,
							fileRequest.Count ?? -1);
					}
					else
					{
						Get(fileRequest.Path, fileRequest.Start);
					}
				}
				else
				{
					throw new FileNotFoundException("This user does not support TTH.");
				}
				return;
			}

			ListRequest listRequest = request as ListRequest;
			if (listRequest != null)
			{
				if (Supports(Features.ADCGet))
				{
					ADCGET("file", "files.xml.bz2", 0, -1);
				}
				else
				{
					Get("files.xml.bz2", 0);
				}
				return;
			}

			HashTreeRequest treeRequest = (HashTreeRequest)request;
			if (treeRequest != null)
			{
				ADCGET("tthl", "TTH/" + treeRequest.TTH, 0, -1);
			}
		}

		protected void Get(string path, long offset)
		{
			// Most clients expects \ as the directory separator.
			path = path.Replace(Path.DirectorySeparatorChar, '\\');

			Send("$Get {0}${1}|", path, offset + 1);
		}

		protected void ADCGET(string type, string identifier, long offset, long count)
		{
			// Here slash is used as the directory separator.
			identifier = identifier.
				Replace(Path.DirectorySeparatorChar, '/').
				Replace(Path.AltDirectorySeparatorChar, '/');

			Send(Encoding.UTF8, "$ADCGET {0} {1} {2} {3}|", type,
				EscapeAdcParameter(identifier), offset, count);
		}

		#endregion

		protected void MaxedOut()
		{
			Send("$MaxedOut|");
		}

		protected static string GetErrorDescription(Errors error)
		{
			switch(error)
			{
				case Errors.FileNotAvailable:
					return "File Not Available";

				case Errors.InvalidCommand:
					return "Invalid Command";

				case Errors.InvalidKey:
					return "Invalid Key";

				case Errors.NoSlots:
					return "No slots available";
			}
			return null;
		}

		protected void Error(Errors error)
		{
			Error(GetErrorDescription(error));
		}

		protected void Error(string argument)
		{
			Send("$Error {0}|", argument);
		}

		protected void Send(string format, params object[] args)
		{
			connection.Send(args != null ? String.Format(format, args) : format);
		}

		protected void Send(Encoding encoding, string format, params object[] args)
		{
			connection.Send(args != null ? String.Format(format, args) : format, encoding);
		}

		protected static string EscapeAdcParameter(string param)
		{
			return param.
				Replace("\\", "\\\\").
				Replace(" ", "\\s").
				Replace("\n", "\\n");
		}

		protected static string UnescapeAdcParameter(string param)
		{
			return param.
				Replace("\\ ", " "). // Used in the old spec
				Replace("\\\\", "\\").
				Replace("\\s", " ").
				Replace("\\n", "\n");
		}

		#endregion
	}
}
